package org.smarthings.scripting;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;

import javax.script.ScriptEngineFactory;
import javax.script.ScriptEngineManager;

import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.ReferenceCardinality;
import org.apache.felix.scr.annotations.ReferencePolicy;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleEvent;
import org.osgi.framework.BundleListener;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.component.ComponentContext;

@Component(metatype = false, immediate = true, specVersion = "1.1")
@Reference(name = "ScriptEngineFactory", referenceInterface = ScriptEngineFactory.class, policy = ReferencePolicy.DYNAMIC, cardinality = ReferenceCardinality.OPTIONAL_MULTIPLE)
public class ScriptEngineFactoryManager implements BundleListener {

	private static final String ENGINE_FACTORY_SERVICE = "META-INF/services/"
			+ ScriptEngineFactory.class.getName();

	private final Set<Bundle> engineResources = new HashSet<Bundle>();
	private final List<ScriptEngineFactory> engineServices = new ArrayList<ScriptEngineFactory>();

	private ScriptEngineManagerProxy scriptEngineManager = new ScriptEngineManagerProxy();
	private ServiceRegistration scriptEngineManagerRegistration;

	public void bindScriptEngineFactory(ScriptEngineFactory scriptEngineFactory) {
		if (scriptEngineFactory != null) {
			synchronized (this.engineServices) {
				this.engineServices.add(scriptEngineFactory);
			}

			this.refreshScriptEngineManager();
		}

	}

	public void unbindScriptEngineFactory(ScriptEngineFactory scriptEngineFactory) {
		boolean refresh;
		synchronized (this.engineServices) {
			refresh = this.engineServices.remove(scriptEngineFactory);
		}

		if (refresh) {
			this.refreshScriptEngineManager();
		}
	}

	public void activate(ComponentContext context) {
		context.getBundleContext().addBundleListener(this);

		Bundle[] bundles = context.getBundleContext().getBundles();
		synchronized (this.engineResources) {
			for (Bundle bundle : bundles) {
				if (bundle.getState() == Bundle.ACTIVE
						&& bundle.getEntry(ENGINE_FACTORY_SERVICE) != null) {
					this.engineResources.add(bundle);
				}
			}
		}

		// create a script engine manager
		this.refreshScriptEngineManager();

		scriptEngineManagerRegistration = context.getBundleContext().registerService(
				ScriptEngineManager.class.getName(), scriptEngineManager, null);
	}

	public void deactivate(ComponentContext context) {
		context.getBundleContext().removeBundleListener(this);

		if (scriptEngineManagerRegistration != null) {
			scriptEngineManagerRegistration.unregister();
			scriptEngineManagerRegistration = null;
		}

		synchronized (this) {
			this.engineResources.clear();
			this.engineServices.clear();
		}

		scriptEngineManager.setDelegate(null);
	}

	@Override
	public void bundleChanged(BundleEvent event) {
		if (event.getType() == BundleEvent.STARTED
				&& event.getBundle().getEntry(ENGINE_FACTORY_SERVICE) != null) {
			synchronized (this.engineResources) {
				this.engineResources.add(event.getBundle());
			}
			this.refreshScriptEngineManager();
		} else if (event.getType() == BundleEvent.STOPPED) {
			boolean refresh;
			synchronized (this.engineResources) {
				refresh = this.engineResources.remove(event.getBundle());
			}
			if (refresh) {
				this.refreshScriptEngineManager();
			}
		}
	}

	/**
	 * Refresh the script engine manager.
	 */
	private void refreshScriptEngineManager() {
		// create (empty) script engine manager
		final ClassLoader loader = getClass().getClassLoader();
		final ScriptEngineManagerDelegate tmp = new ScriptEngineManagerDelegate(loader);

		// register script engines from bundles
		synchronized (this.engineResources) {
			for (Bundle bundle : this.engineResources) {
				registerFactories(tmp, bundle);
			}
		}

		// register script engines from registered services
		synchronized (this.engineServices) {
			for (ScriptEngineFactory factory : this.engineServices) {
				tmp.registerScriptEngineFactory(factory);
			}
		}

		scriptEngineManager.setDelegate(tmp);

	}

	@SuppressWarnings("unchecked")
	private void registerFactories(ScriptEngineManagerDelegate delegate, Bundle bundle) {
		URL url = bundle.getEntry(ENGINE_FACTORY_SERVICE);
		InputStream ins = null;
		final SortedSet<String> extensions = new TreeSet<String>();
		try {
			ins = url.openStream();
			BufferedReader reader = new BufferedReader(new InputStreamReader(ins));
			String line;
			while ((line = reader.readLine()) != null) {
				if (!line.startsWith("#") && line.trim().length() > 0) {
					try {
						Class<ScriptEngineFactory> clazz = bundle.loadClass(line);
						ScriptEngineFactory spi = clazz.newInstance();
						delegate.registerScriptEngineFactory(spi);
						extensions.addAll(spi.getExtensions());
					} catch (Throwable t) {
						t.printStackTrace();
					}
				}
			}
		} catch (IOException ioe) {
			// ignore
		} finally {
			if (ins != null) {
				try {
					ins.close();
				} catch (IOException ioe) {
				}
			}
		}

	}

}
